package org.beanutopia;

import static org.beanutopia.function.CompositeFunction.self;
import org.beanutopia.constraint.Constraint;
import org.beanutopia.exception.PropertyDefinitionException;
import org.beanutopia.function.*;
//import com.cisco.swiper.property.compose.InjectionRules;
import static org.beanutopia.function.PropertyFunctions.isInstanceOf;
import org.beanutopia.slot.Slot;
import org.beanutopia.slot.ExtraSlot;
import org.beanutopia.reflect.ReflectSlot;
import org.beanutopia.htmap.HasProperties;
import org.beanutopia.htmap.HTSlot;
import com.google.common.base.Function;
import com.google.common.base.Nullable;
import com.google.common.base.Predicate;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * @author ymeymann
 * @since Apr 29, 2007 12:37:39 PM
 */
public abstract class UpdatableProperty<B,V> extends Property<B,V> {
  private List<Constraint<? super V>> constraints;
  protected Policy<B,Injection<? super B, ? super V>> injection = new Policy<B,Injection<? super B, ? super V>>();

  public UpdatableProperty() {
    super(null);
  }

  public UpdatableProperty(V defaultValue) {
    super(defaultValue);
    //initAccessRules();
  }

  public UpdatableProperty(V defaultValue, Constraint<? super V>... constraints) {
    super(defaultValue);
    constrain(constraints);
    //initAccessRules();
  }

  private void constrain(Constraint<? super V>... constraints) {
    this.constraints = Arrays.asList(constraints);
    //fill in constraints
    for (Constraint<? super V> c: constraints()) {
      c.validate(defaultValue(),  this);
    }
  }

  @Override
  protected void init() {
    if (!projection.isEmpty() && !injection.isEmpty()) return;
    resetDefinition();
  }

  @Override
  public void resetDefinition() {
    projection.clear();
    injection.clear();

    projection.addNullSafetyRule();

    Slot<Object,V> reflector = ReflectSlot.reflector(beanType(), name, valueType(), valueType() == Boolean.class ? "is" : "get", "set");
    if (reflector.supported().apply(beanType())) {
      projection.addRule(reflector.getProjection());
      injection.addRule(reflector.getInjection());
    } else if (HasProperties.class.isAssignableFrom(beanType())) {
      Slot<HasProperties,V> intraSlot = intraAccess();
      projection.addRule(isInstanceOf(HasProperties.class), self().cast(HasProperties.class).compose(intraSlot.getProjection()));
      injection.addRule(isInstanceOf(HasProperties.class), self().cast(HasProperties.class).inject(intraSlot.getInjection()));
    } else {
      projection.addRule(reflector.supported(), reflector.getProjection());
      injection.addRule(reflector.supported(), reflector.getInjection());

      //TODO enforce order between rules
      Slot<HasProperties,V> intraSlot = intraAccess();
      projection.addRule(isInstanceOf(HasProperties.class), self().cast(HasProperties.class).compose(intraSlot.getProjection()));
      injection.addRule(isInstanceOf(HasProperties.class), self().cast(HasProperties.class).inject(intraSlot.getInjection()));

      Slot<Object,V> extraSlot = extraAccess();
      projection.addRule(extraSlot.getProjection());
      injection.addRule(extraSlot.getInjection());
    }
    projection.trimToSize();
  }


  public List<Constraint<? super V>> constraints() {
    return constraints != null ? constraints : Collections.<Constraint<? super V>>emptyList(); 
  }

  public void addConstraint(Constraint<? super V> constraint) {
    constraints.add(constraint);
  }

  public class Value implements Function<B, B> { //extends Property<B,V>.Value

    V value = null;

    protected Value(V value) {
      this.value = value;
    }

    public V getValue() {
      return value;
    }
    public void setValue(V value) {
      this.value = value;
    }

    public UpdatableProperty<B, V> getProperty() { return UpdatableProperty.this; }//(UpdatableProperty<B, V>) super.getProperty();

    public B apply(B object) {
      getProperty().assign(object, getValue());
      return object;
    }
  }

  public Value value(V value) {
    return new Value(value);
  }

  public final void assign(B object, V value) {
    initialize();    
//    if (value != null && !valueType().isAssignableFrom(value.getClass())) {
//      throw new ClassCastException("property " + name() + " does not support " + value.getClass() + " values");
//    }
//    if (object != null && !beanType().isAssignableFrom(object.getClass())) {
//      throw new ClassCastException("property " + name() + " is not applicable to " + object.getClass() + " property");
//    }
    for (Constraint<? super V> c: constraints()) {
      c.validate(value,  this);
    }
//    injector((Class<? extends B>)object.getClass()).apply(object, value);
    set(object, value);
  }

  protected void set(B object, @Nullable V value) {
    Injection<? super B, ? super V> injector = injector();//added
    if (injector != null)
      injector.apply(object, value);
  }

  public final void clear(B object) {
    initialize();    
    if (object != null && !beanType().isAssignableFrom(object.getClass())) {
      throw new ClassCastException("property " + name() + " is not applicable to " + object.getClass() + " property");
    }
    set(object, null);
  }

  private final Injection<B, V> myInjection = new Injection<B,V> (){
    public void apply(B object, V value) {
      Injection<? super B, ? super V> localInjection = injection.apply(object);
      if (localInjection != null) localInjection.apply(object, value);
    }
  };

  public final Injection<? super B, ? super V> injector() {
    return myInjection;
  }

  @SuppressWarnings({"unchecked"})
  protected void setInjection(Injection<? super B, ? super V> injection) {
    this.injection.clear();
    this.injection.addRule(injection);
  }

  <C extends B> void redefine(Class<C> cl, Injection<? super C, ? super V> injection) {
    this.injection.addRule(isInstanceOf(cl), self().cast(cl).inject(injection));
  }

  void redefine(Class<? extends B> cl, Function<? super B, ? extends V> projection,
          Injection<? super B, ? super V> injection) {
    redefine(cl, projection);
    redefine(cl, injection);
  }


  void define(Function<? super B, ? extends V> projection,
          Injection<? super B, ? super V> injection) {
    setProjection(projection);
    setInjection(injection);
  }

  public void define(Slot slot) {
    define(slot.getProjection(), slot.getInjection());
  }

  @SuppressWarnings({"unchecked"})
  public void redefine(Class<? extends B> cl, Slot slot) {
    redefine(cl, slot.getProjection());
    redefine(cl, slot.getInjection());
  }

  public Slot access() {
    return new Slot() {
      @SuppressWarnings({"unchecked"})
      public Function<B, V> getProjection() {
        return (Function<B, V>) projector();
      }
      @SuppressWarnings({"unchecked"})
      public Injection<B, V> getInjection() {
        return (Injection<B, V>) injector();
      }
      public Slot initialize() throws PropertyDefinitionException {
        UpdatableProperty.this.initialize();
        return this;
      }
      public Predicate<Object> supported() {
        return isInstanceOf(beanType());
      }
    };
  }

  final Slot<HasProperties,V> intraAccess() {
    return new HTSlot<V>(this);
  }

  final Slot<Object,V> extraAccess() {
    return new ExtraSlot<V>();
  }

}
